NunjucksTemplates   A
last analyzed

Complexity

Total Complexity 13

Size/Duplication

Total Lines 126
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 101
dl 0
loc 126
rs 10
c 0
b 0
f 0

4 Functions

Rating   Name   Duplication   Size   Complexity  
A render 0 3 1
A markSafe 0 3 1
B createEnv 0 45 5
B registerViewEngine 0 62 6
1
import * as crypto from 'crypto';
2
import { Inject, Injectable } from '@nestjs/common';
3
import { NextFunction, Request, Response } from 'express';
4
import { NestExpressApplication } from '@nestjs/platform-express';
5
import { Environment, FileSystemLoader, TemplateCallback } from 'nunjucks';
6
import { runtime } from 'nunjucks';
7
import { ITemplates } from '../ITemplates';
8
import { RouteNameResolver } from '../../Common/ExtendedRouting/RouteNameResolver';
9
import { ITranslator } from 'src/Infrastructure/Translations/ITranslator';
10
import { formatFullName } from '../../Common/Utils/formatUtils';
11
import {
12
  formatDate,
13
  formatEventDate,
14
  formatHtmlDate,
15
  formatHtmlYearMonth,
16
  minutesToHours
17
} from '../../Common/Utils/dateUtils';
18
import { ArrayUtils } from '../../Common/Utils/ArrayUtils';
19
import { TablesExtension } from './TablesExtension';
20
import { getYear } from 'date-fns';
21
22
@Injectable()
23
export class NunjucksTemplates implements ITemplates {
24
  private env: Environment;
25
26
  constructor(
27
    private readonly resolver: RouteNameResolver,
28
    @Inject('ITranslator')
29
    private readonly translator: ITranslator
30
  ) {}
31
32
  private createEnv(viewsDir: string): Environment {
33
    const loader = new FileSystemLoader(viewsDir, {
34
      watch: process.env.NODE_ENV !== 'production'
35
    });
36
    const env = new Environment(loader);
37
38
    env.addFilter('trans', (key, params = {}) =>
39
      this.translator.translate(key, params)
40
    );
41
    env.addFilter('startswith', (value: string | null, other: string) =>
42
      value ? value.startsWith(other) : false
43
    );
44
    env.addFilter('minutesToHours', minutes => minutesToHours(minutes));
45
    env.addFilter('fullName', obj => formatFullName(obj));
46
    env.addFilter('date', value =>
47
      value === 'now' ? new Date() : formatDate(value)
48
    );
49
    env.addFilter('eventDate', value => formatEventDate(value));
50
    env.addFilter('htmlDate', value => formatHtmlDate(value));
51
    env.addFilter('htmlYearMonth', value => formatHtmlYearMonth(value));
52
    env.addFilter('longMonth', (month: number) =>
53
      this.translator.translate('common-month-long', {
54
        date: new Date(2023, month, 15)
55
      })
56
    );
57
    env.addFilter('year', (date: Date) => getYear(date));
58
    env.addFilter('zip', (left, right) => ArrayUtils.zip(left, right));
59
    env.addFilter('merge', (left, right) => {
60
      const result = {};
61
      for (const key in left) {
62
        result[key] = left[key];
63
      }
64
      for (const key in right) {
65
        result[key] = right[key];
66
      }
67
      return result;
68
    });
69
    env.addGlobal('randomHex', (size: number) => {
70
      return crypto.randomBytes(size).toString('hex');
71
    });
72
73
    env.addExtension('tables', new TablesExtension(this));
74
75
    return env;
76
  }
77
78
  registerViewEngine(app: NestExpressApplication, assetsRoot: string): void {
79
    const express = app.getHttpAdapter().getInstance();
80
81
    this.env = this.createEnv(express.get('views'));
82
83
    app.use((req: Request, res: Response, next: NextFunction): void => {
84
      const ctx = (res.locals.njkCtx = res.locals.njkCtx ?? {});
85
86
      ctx['req'] = req;
87
88
      ctx['path'] = (name: string, params: object = {}) => {
89
        try {
90
          return this.resolver.resolve(name, params);
91
        } catch (err) {
92
          console.error(
93
            `Failed to resolve path ${name} with params ${JSON.stringify(
94
              params
95
            )}: ${err}`
96
          );
97
          return '#';
98
        }
99
      };
100
101
      ctx['url'] = (name: string, params: object = {}) => {
102
        try {
103
          const path = this.resolver.resolve(name, params);
104
          const proto = req.protocol;
105
          const origin = req.get('Host');
106
          const baseUrl = `${proto}://${origin}`;
107
          const url = new URL(path, baseUrl);
108
          return url.toString();
109
        } catch {
110
          return '#';
111
        }
112
      };
113
114
      ctx['view_name'] = this.resolver.getName(req.url);
115
116
      ctx['asset'] = (path: string) => {
117
        return `${assetsRoot === '/' ? '' : assetsRoot}/${path}`;
118
      };
119
120
      ctx['now'] = new Date();
121
122
      ctx['theme'] = req.cookies.theme;
123
124
      next();
125
    });
126
127
    app.engine(
128
      'njk',
129
      (
130
        name: string,
131
        ctx: Record<string, any>,
132
        cb: TemplateCallback<string>
133
      ) => {
134
        this.env.render(name, { ...ctx, ...ctx._locals.njkCtx }, cb);
135
      }
136
    );
137
138
    app.setViewEngine('njk');
139
  }
140
141
  public render(name: string, context: object): string {
142
    return this.env.render(name, context);
143
  }
144
145
  public markSafe(html: string): any {
146
    return new runtime.SafeString(html);
147
  }
148
}
149